/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.core.converter.converter;

import com.je.bpm.core.converter.converter.export.*;
import com.je.bpm.core.converter.exceptions.XMLException;
import com.je.bpm.core.model.*;
import com.je.bpm.core.model.artifact.Artifact;
import com.je.bpm.core.model.process.SubProcess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.je.bpm.core.model.process.Process;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.util.*;

public class SubprocessXMLConverter extends BpmnXMLConverter {

    protected static final Logger LOGGER = LoggerFactory.getLogger(SubprocessXMLConverter.class);
    
    @Override
    public byte[] convertToXML(BpmnModel model, String encoding) {
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            XMLOutputFactory xof = XMLOutputFactory.newInstance();
            OutputStreamWriter out = new OutputStreamWriter(outputStream, encoding);
            XMLStreamWriter writer = xof.createXMLStreamWriter(out);
            XMLStreamWriter xtw = new IndentingXMLStreamWriter(writer);
            DefinitionsRootExport.writeRootElement(model, xtw, encoding);
            CollaborationExport.writePools(model, xtw);
            DataStoreExport.writeDataStores(model, xtw);
            SignalAndMessageDefinitionExport.writeSignalsAndMessages(model, xtw);
            for (Process process : model.getProcesses()) {
                if (process.getFlowElements().isEmpty() && process.getLanes().isEmpty()) {
                    // empty process, ignore it
                    continue;
                }
                ProcessExport.writeProcess(process, xtw);
                for (FlowElement flowElement : process.getFlowElements()) {
                    createXML(flowElement, model, xtw);
                }
                for (Artifact artifact : process.getArtifacts()) {
                    createXML(artifact, model, xtw);
                }
                // end process element
                xtw.writeEndElement();
            }

            // refactor each subprocess into a separate Diagram
            List<BpmnModel> subModels = parseSubModels(model);
            for (BpmnModel tempModel : subModels) {
                if (!tempModel.getFlowLocationMap().isEmpty() || !tempModel.getLocationMap().isEmpty()) {
                    BPMNDIExport.writeBPMNDI(tempModel, xtw);
                }
            }

            // end definitions root element
            xtw.writeEndElement();
            xtw.writeEndDocument();

            xtw.flush();
            byte[] bytes = outputStream.toByteArray().clone();

            // cleanup
            outputStream.close();
            xtw.close();

            return bytes;
        } catch (Exception e) {
            LOGGER.error("Error writing BPMN XML", e);
            throw new XMLException("Error writing BPMN XML", e);
        }
    }

    protected List<BpmnModel> parseSubModels(BpmnModel model) {
        List<BpmnModel> subModels = new ArrayList<BpmnModel>();
        // find all subprocesses
        Collection<FlowElement> flowElements = model.getMainProcess().getFlowElements();
        Map<String, GraphicInfo> locations = new HashMap<String, GraphicInfo>();
        Map<String, List<GraphicInfo>> flowLocations = new HashMap<String, List<GraphicInfo>>();
        Map<String, GraphicInfo> labelLocations = new HashMap<String, GraphicInfo>();

        locations.putAll(model.getLocationMap());
        flowLocations.putAll(model.getFlowLocationMap());
        labelLocations.putAll(model.getLabelLocationMap());

        // include main process as separate model
        BpmnModel mainModel = new BpmnModel();
        // set main process in submodel to subprocess
        mainModel.addProcess(model.getMainProcess());

        String elementId = null;
        for (FlowElement element : flowElements) {
            elementId = element.getId();
            if (element instanceof SubProcess) {
                subModels.addAll(parseSubModels(element, locations, flowLocations, labelLocations));
            }
            if (element instanceof SequenceFlow && null != flowLocations.get(elementId)) {
                // must be an edge
                mainModel.getFlowLocationMap().put(elementId, flowLocations.get(elementId));
            } else {
                // do not include data objects because they do not have a corresponding shape in the BPMNDI data
                if (!(element instanceof DataObject) && null != locations.get(elementId)) {
                    // must be a shape
                    mainModel.getLocationMap().put(elementId, locations.get(elementId));
                }
            }
            // also check for any labels
            if (null != labelLocations.get(elementId)) {
                mainModel.getLabelLocationMap().put(elementId, labelLocations.get(elementId));
            }
        }
        // add main process model to list
        subModels.add(mainModel);
        return subModels;
    }

    private List<BpmnModel> parseSubModels(FlowElement subElement, Map<String, GraphicInfo> locations, Map<String, List<GraphicInfo>> flowLocations, Map<String, GraphicInfo> labelLocations) {
        List<BpmnModel> subModels = new ArrayList<BpmnModel>();
        BpmnModel subModel = new BpmnModel();
        String elementId = null;

        // find nested subprocess models
        Collection<FlowElement> subFlowElements = ((SubProcess) subElement).getFlowElements();
        // set main process in submodel to subprocess
        Process newMainProcess = new Process();
        newMainProcess.setId(subElement.getId());
        newMainProcess.getFlowElements().addAll(subFlowElements);
        newMainProcess.getArtifacts().addAll(((SubProcess) subElement).getArtifacts());
        subModel.addProcess(newMainProcess);

        for (FlowElement element : subFlowElements) {
            elementId = element.getId();
            if (element instanceof SubProcess) {
                subModels.addAll(parseSubModels(element, locations, flowLocations, labelLocations));
            }
            if (element instanceof SequenceFlow && null != flowLocations.get(elementId)) {
                // must be an edge
                subModel.getFlowLocationMap().put(elementId, flowLocations.get(elementId));
            } else {
                // do not include data objects because they do not have a corresponding shape in the BPMNDI data
                if (!(element instanceof DataObject) && null != locations.get(elementId)) {
                    // must be a shape
                    subModel.getLocationMap().put(elementId, locations.get(elementId));
                }
            }
            // also check for any labels
            if (null != labelLocations.get(elementId)) {
                subModel.getLabelLocationMap().put(elementId, labelLocations.get(elementId));
            }
        }
        subModels.add(subModel);
        return subModels;
    }
}
